package utils;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.http.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.AbstractVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * Created by zhangyupeng on 2015/6/1.
 */
public class HttpHelper {
    private static Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class);
    private static Logger HTTP_LOGGER = LoggerFactory.getLogger("http.api.logger");
    private static PoolingHttpClientConnectionManager connManager;
    private static final LinkedList<BasicHeader> headers = new LinkedList<BasicHeader>();

    private static final int MAX_CONNECTION_COUNT = 100;

    //TODO 为适应中民接口，暂改为10秒超时
    private static final int CONNECT_TIMEOUT = 20000; // ms
    private static final int SOCKET_TIMEOUT = 20000; // ms
    private static final int CONNECTION_REQUEST_TIMEOUT = 20000; // ms

    private static final int WAIT_MILLI_SECONDS = 100; //ms
    private static final int MAX_RETRY = 1;

    public static final String TEXT_TYPE = "application/text";


    static {
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            X509TrustManager tm = new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }

                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }

                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
            };
            sslContext.init(null, new TrustManager[]{tm}, null);
            SSLConnectionSocketFactory sslConnectionSocketFactory =
                    new SSLConnectionSocketFactory(sslContext, new AbstractVerifier() {
                        public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException {
                            if (!host.endsWith("trade.95549.cn")) {
                                SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER.verify(host, cns, subjectAlts);
                            }
                        }
                    });
            Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
                    .<ConnectionSocketFactory>create()
                    .register("https", sslConnectionSocketFactory)
                    .register("http", new PlainConnectionSocketFactory())
                    .build();
            connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
            connManager.setMaxTotal(MAX_CONNECTION_COUNT);
            connManager.setDefaultMaxPerRoute(MAX_CONNECTION_COUNT);
        } catch (Exception e) {
            LOGGER.error("failed to get connection manage {}", ExceptionUtils.getStackTrace(e));
        }

        headers.add(new BasicHeader(HttpHeaders.ACCEPT, "*/*"));
        headers.add(new BasicHeader(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.81 Safari/537.36"));
    }


    private synchronized static CloseableHttpClient getHttpClient() {
        return getHttpClient(connManager);
    }

    public synchronized static CloseableHttpClient getHttpClient(final HttpClientConnectionManager connectionManager) {
        SocketConfig socketConfig = SocketConfig.custom()
                .setSoTimeout(SOCKET_TIMEOUT)
                .build();
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(CONNECT_TIMEOUT)
                .setSocketTimeout(SOCKET_TIMEOUT)
                .setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT)
                .build();
        CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig)
                .setDefaultSocketConfig(socketConfig)
                .setDefaultHeaders(headers)
                .build();
        return httpClient;
    }

    public static HttpResult postFormBody(String url, Map<String, String> params) throws Exception {
        List<NameValuePair> formParams = map2FormParams(params);
        UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, "UTF-8");
        return post(url, entity);
    }

    public static <T> T asyncPostFormBody(String url, Map<String, String> params, ResponseHandler<T> handler) throws Exception {
        List<NameValuePair> formParams = map2FormParams(params);
        UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, "UTF-8");
        return asyncPost(url, entity, handler);
    }

    public static HttpResult getFormBody(String url, Map<String, String> params) throws Exception {
        List<NameValuePair> formParams = map2FormParams(params);
        return get(url, formParams, getHttpClient());
    }

    public static HttpResult get(String url) throws Exception {
        return get(url, null, getHttpClient());
    }

    public static HttpResult postJson(String url, String params) throws Exception {
        StringEntity entity = new StringEntity(params, "UTF-8");
        entity.setContentType("application/json; charset=UTF-8");
        return post(url, entity);
    }

    public static HttpResult tkpost(String url, String params) throws Exception {
        StringEntity entity = new StringEntity(params, "gbk");
        entity.setContentType("application/text; charset=gbk");
        return post(url, entity);
    }

    public static HttpResult postXML(String url, String postBody) throws Exception {
        StringEntity entity = new StringEntity(postBody, "UTF-8");
        entity.setContentType("text/xml; charset=UTF-8");
        return post(url, entity);
    }

    public static HttpResult postXML(String url, String postBody, String charSet) throws Exception {
        StringEntity entity = new StringEntity(postBody, charSet);
        entity.setContentType("text/xml; charset=" + charSet);
        return post(url, entity);
    }

    public static HttpResult post(String url, String postBody) throws Exception {
        StringEntity entity = new StringEntity(postBody, "UTF-8");
        return post(url, entity);
    }

    public static HttpResult post(String url, String postBody, String charSet, String contentType) throws Exception {
        StringEntity entity = new StringEntity(postBody, charSet);
        entity.setContentType(contentType + "; charset=" + charSet);
        return post(url, entity);
    }

    public static <T> T asyncPost(String url, String postBody, ResponseHandler<T> handler) throws Exception {
        StringEntity entity = new StringEntity(postBody, "UTF-8");
        return asyncPost(url, entity, handler);
    }

    public static <T> T asyncPost(String url, HttpEntity entity, ResponseHandler<T> handler) throws Exception {
        try {
            HttpPost httpPost = new HttpPost(url);
            httpPost.setEntity(entity);
            long begin = System.currentTimeMillis();
            T result = asyncHttpCall(getHttpClient(), httpPost, handler, MAX_RETRY);
            LOGGER.info("url: {}, postBody: {}, result: {}, cost: {}",
                    new Object[]{url, EntityUtils.toString(entity), result, System.currentTimeMillis() - begin});
            return result;
        } catch (Exception e) {
            LOGGER.error("post fail: {} {} {}", url, EntityUtils.toString(entity), ExceptionUtils.getStackTrace(e));
            throw e;
        }
    }

    public static HttpResult post(String url, HttpEntity entity) throws Exception {
        return post(url, entity, getHttpClient());
    }


    public static HttpResult post(String url, HttpEntity entity, HttpClient httpClient) throws Exception {
        try {
            HttpPost httpPost = new HttpPost(url);
            httpPost.setEntity(entity);
            long begin = System.currentTimeMillis();
            HttpResponse httpResponse = doHttpCall(httpClient, httpPost, MAX_RETRY);
            long end = System.currentTimeMillis();
            //统计http请求耗时
            int statusCode = httpResponse.getStatusLine().getStatusCode();
            String content = EntityUtils.toString(httpResponse.getEntity(), "UTF-8");
            //TODO 模糊化证件号
            LOGGER.info("url: {}, postBody: {}, statusCode: {}, responseBody: {}, cost: {}",
                    new Object[]{url, EntityUtils.toString(entity), statusCode, content, end - begin});
            return new HttpResult(statusCode, content);
        } catch (Exception e) {
            LOGGER.error("post fail: {}, {} {}", url, EntityUtils.toString(entity), ExceptionUtils.getStackTrace(e));
            throw e;
        }
    }


    public static HttpResult get(String url, List<NameValuePair> params, HttpClient httpClient) throws Exception {
        try {
            if (params != null && params.size() > 0) {
                url = url + "?" + URLEncodedUtils.format(params, "UTF-8");
            }
            HttpGet httpGet = new HttpGet(url);
            long begin = System.currentTimeMillis();
            HttpResponse httpResponse = doHttpCall(httpClient, httpGet, MAX_RETRY);
            long end = System.currentTimeMillis();
            //统计http请求耗时
            int statusCode = httpResponse.getStatusLine().getStatusCode();
            String content = EntityUtils.toString(httpResponse.getEntity(), "UTF-8");
            //TODO 模糊化证件号
            LOGGER.info("url: {}, statusCode: {}, responseBody: {}, cost: {}",
                    new Object[]{url, statusCode, content, end - begin});
            return new HttpResult(statusCode, content);
        } catch (Exception e) {
            LOGGER.error("http fail: {} {} {}", url, ArrayUtils.toString(params), ExceptionUtils.getStackTrace(e));
            throw e;
        }
    }

    private static PoolingHttpClientConnectionManager downLoadConnManager = new PoolingHttpClientConnectionManager();

    static {
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            X509TrustManager tm = new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }

                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }

                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
            };
            sslContext.init(null, new TrustManager[]{tm}, null);
            SSLConnectionSocketFactory sslConnectionSocketFactory =
                    new SSLConnectionSocketFactory(sslContext, new AbstractVerifier() {
                        public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException {
                            if (!host.endsWith("zhongan.com")) {
                                SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER.verify(host, cns, subjectAlts);
                            }
                        }
                    });
            Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
                    .<ConnectionSocketFactory>create().register("https", sslConnectionSocketFactory)
                    .build();


            downLoadConnManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
            downLoadConnManager.setMaxTotal(MAX_CONNECTION_COUNT);
            downLoadConnManager.setDefaultMaxPerRoute(MAX_CONNECTION_COUNT);
        } catch (Exception e) {
            LOGGER.error("failed to get connection manage {}", ExceptionUtils.getStackTrace(e));
        }
    }

    public static HttpFileDownloadResult getFileDownload(String url, String targetContentType) throws Exception {
        HttpGet httpGet = new HttpGet(url);
        HttpResponse httpResponse;
        if (url.contains("https")) {
            httpResponse = doHttpCall(getHttpClient(downLoadConnManager), httpGet, MAX_RETRY);
        } else {
            httpResponse = doHttpCall(getHttpClient(connManager), httpGet, MAX_RETRY);
        }

        HttpFileDownloadResult result = new HttpFileDownloadResult();

        int statusCode = httpResponse.getStatusLine().getStatusCode();
        result.setStatusCode(statusCode);

        if (statusCode >= 300 || statusCode < 200) {
            LOGGER.info("url: {}, statusCode: {} error.", url, statusCode);
            httpGet.abort(); //主动释放连接
            result.setSuccess(false);
            return result;
        }

        String filename = getAttachFilename(httpResponse);

        if (filename == null
                && !isContentTypeMatch(httpResponse, targetContentType)) { //不是附件 & 不是目标类型
            LOGGER.info("url: {}, contentType: {} not match targetContentType: {} and not attachment.",
                    url, httpResponse.getFirstHeader("Content-Type"), targetContentType);
            httpGet.abort(); //主动释放连接
            result.setSuccess(false);
            return result;
        }

        result.setSuccess(true);
        if (StringUtils.endsWith(filename, ".zip")) {
            LOGGER.info("zip file: {}", filename);
            ZipInputStream zis = new ZipInputStream(httpResponse.getEntity().getContent());
            ZipEntry entry = zis.getNextEntry();
            result.setFilename(entry.getName());
            result.setInputStream(zis);
        } else {
            result.setFilename(filename);
            result.setInputStream(httpResponse.getEntity().getContent());
        }

        return result;
    }

    private static boolean isContentTypeMatch(HttpResponse httpResponse, String targetContentType) {
        if (StringUtils.isBlank(targetContentType)) { //为空认为不限制
            return true;
        } else {
            Header contentType = httpResponse.getFirstHeader("Content-Type");
            return contentType != null && StringUtils.startsWithIgnoreCase(contentType.getValue(), targetContentType); //检查类型匹配
        }
    }

    private static String getAttachFilename(HttpResponse httpResponse) {
        Header contentDisposition = httpResponse.getFirstHeader("Content-Disposition");
        if (contentDisposition == null || contentDisposition.getElements().length < 1) {
            return null;
        }

        HeaderElement headerElement = contentDisposition.getElements()[0];
        if (!"attachment".equalsIgnoreCase(headerElement.getName())) {//不是下载附件
            return null;
        } else {
            return headerElement.getParameterByName("filename").getValue();
        }
    }


    private static <T> T asyncHttpCall(HttpClient httpclient, HttpUriRequest httpRequest, ResponseHandler<T> handler, int maxRetry) throws Exception {
        List<Class<? extends Exception>> exceptionTypes = new ArrayList<Class<? extends Exception>>();
        exceptionTypes.add(IOException.class);
        int retryCount = 1;
        while (retryCount <= maxRetry) {
            try {
                T httpResponse = httpclient.execute(httpRequest, handler);
                return httpResponse;
            } catch (Exception ex) {
                httpRequest.abort(); //主动释放连接
                boolean needRetry = false;
                if (retryCount < maxRetry) {
                    for (Class<? extends Exception> exceptionType : exceptionTypes) {
                        if (exceptionType.isInstance(ex)) {
                            needRetry = true;
                        }
                    }
                }
                if (needRetry) {
                    HTTP_LOGGER.warn("retry call exception: {} , times: {}", ex, retryCount);
                    Thread.sleep(retryCount * WAIT_MILLI_SECONDS); //随着重试次数增多，sleep时间变长
                    retryCount++;
                } else {
                    HTTP_LOGGER.error("retry call fails {}", ExceptionUtils.getStackTrace(ex));
                    throw ex;
                }
            }
        }
        return null;
    }

    private static HttpResponse doHttpCall(HttpClient httpclient, HttpUriRequest httpRequest, int maxRetry) throws Exception {
        List<Class<? extends Exception>> exceptionTypes = new ArrayList<Class<? extends Exception>>();
        exceptionTypes.add(IOException.class);
        int retryCount = 1;
        while (retryCount <= maxRetry) {
            try {
                HttpResponse httpResponse = httpclient.execute(httpRequest);
                return httpResponse;
            } catch (Exception ex) {
                httpRequest.abort(); //主动释放连接
                boolean needRetry = false;
                if (retryCount < maxRetry) {
                    for (Class<? extends Exception> exceptionType : exceptionTypes) {
                        if (exceptionType.isInstance(ex)) {
                            needRetry = true;
                        }
                    }
                }
                if (needRetry) {
                    HTTP_LOGGER.warn("retry call exception: {} , times: {}", ex, retryCount);
                    Thread.sleep(retryCount * WAIT_MILLI_SECONDS); //随着重试次数增多，sleep时间变长
                    retryCount++;
                } else {
                    HTTP_LOGGER.error("retry call fails", ex);
                    throw ex;
                }
            }
        }
        return null;
    }

    private static List<NameValuePair> map2FormParams(Map<String, String> params) {
        List<NameValuePair> formParams = new ArrayList<NameValuePair>();
        for (Map.Entry<String, String> entry : params.entrySet()) {
            formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
        }
        return formParams;
    }

    public static void fileUpload() throws Exception {
        MultipartEntityBuilder builder = MultipartEntityBuilder.create();
        builder.addBinaryBody("file", "test body...".getBytes("UTF-8"), ContentType.DEFAULT_BINARY, "billDetails");

        HttpEntity httpEntity = builder.build();

        HttpResult httpResult = post("http://staging.notify.insurance.mifi.n.xiaomi.com/bill/upload?partnerId=102&billDate=xx&billMD5=xxx", httpEntity);
        System.out.println(httpResult);

    }
}
